/**
 * 
 */
package util;

/**
 * @author Tarl
 *
 */
public class RenderMatrix {

	private static int SIZE = 4;
	private double[][] matrix;
	
	// generates an identy matrix.
	public RenderMatrix() {
		matrix = new double[][]{{1,0,0,0},
								{0,1,0,0},
								{0,0,1,0},
								{0,0,0,1}};
	}
	
	private RenderMatrix(double[][] data) {
		matrix = new double[SIZE][SIZE];
		for(int row = 0; row < SIZE; row++) {
			for(int col = 0; col < SIZE; col++) {
				matrix[row][col] = data[row][col];
			}
		}
	}
	
	public String toString() {
		final StringBuilder build = new StringBuilder();
		for (int i = 0; i < SIZE; i++) {
			build.append("[" + matrix[i][0] + ", "+ matrix[i][1] + ", "+ matrix[i][2] + ", "+ matrix[i][3] + "]\n");
		}
		build.deleteCharAt(build.length() - 1); // remove the n
		return build.toString();
	}
	
	/**
	 * Multiplies the matrices together by standard matrix multiplication.
	 * @param rightMatrix The right hand matrix in the multiplication
	 * @return A new matrix [A] X [B] where A is "this" matrix and B is 
	 *         the parameter.
	 */
	public RenderMatrix multiply(final RenderMatrix rightMatrix) {
		final RenderMatrix result = new RenderMatrix();
		
		// multiply for loops
		for (int i = 0; i < SIZE; i++) {
			for (int j = 0; j < SIZE; j++) {
				result.matrix[i][j] = (matrix[i][0] * rightMatrix.matrix[0][j]) + 
									  (matrix[i][1] * rightMatrix.matrix[1][j]) + 
									  (matrix[i][2] * rightMatrix.matrix[2][j]) + 
									  (matrix[i][3] * rightMatrix.matrix[3][j]); 
			}
		}
		return result;
//		double[][] result = new double[4][4];
//		for(int row = 0; row < 4; row++) {//rows in transformation matrix
//			for(int col = 0; col < 4; col++) {//cols in transformation matrix
//				result[row][col] = 0;
//				for(int index = 0; index < 4; index++) {
//					result[row][col] = result[row][col] + this.matrix[row][index] * rightMatrix.matrix[index][col];
//				}
//			}
//		}		
//		return new RenderMatrix(result);
	}
	
	/**
	 * Multiples [A] x V where [A] is a matrix and V is a vector of length 4.
	 * @param vector A vector of length 4.
	 * @return The product of [A] x V.
	 * @throws IllegalArgumentException if the param is not of length 4
	 */
	public double[] multiply(final double[] vector) throws IllegalArgumentException {
		if (vector.length != 4) throw new IllegalArgumentException("parameter length is not 4");
//		final double[] result = {0, 0, 0, 0};
//		
//		// //System.out.println("Trans\n" + toString());
//		// //System.out.println("Before\n" + vectorToString(vector));
//		// multiplies the the vector and returns a new double array.
//		for (int i = 0; i < SIZE; i++) {
//			result[i] = (matrix[i][0] * vector[0]) + 
//						(matrix[i][1] * vector[1]) + 
//						(matrix[i][2] * vector[2]) + 
//						(matrix[i][3] * vector[3]); 
//		}
//		result[0] = result[0] / result[3];
//		result[1] = result[1] / result[3];
//		result[2] = result[2] / result[3];
//		result[3] = 1;
		
		// //System.out.println("After\n" + vectorToString(result));
		//return result;
		
		double[] result = new double[4];
		for(int row = 0; row < 4; row++) {
			result[row] = 0;
			for(int col = 0; col < 4; col++) {
				result[row] += matrix[row][col] * vector[col];
			}
		}
		result[0] = result[0] / result[3];
		result[1] = result[1] / result[3];
		result[2] = result[2] / result[3];
		result[3] = 0;
		return result;
		
	}

	/**
	 * Builds a scaling matrix
	 * @param s1 Scale along x axis
	 * @param s2 Scale along y axis
	 * @param s3 Scale along z axis
	 * @return A matrix that scales along the 3 axis.
	 */
	public static RenderMatrix buildScale(final double s1, final double s2, final double s3) {
		final RenderMatrix result = new RenderMatrix();
		result.matrix[0][0] = s1;
		result.matrix[1][1] = s2;
		result.matrix[2][2] = s3;
		return result;
	}
	
	/**
	 * Builds a translation matrix
	 * @param s1 translate along x axis
	 * @param s2 translate along y axis
	 * @param s3 translate along z axis
	 * @return A matrix that translates along the 3 axis.
	 */
	public static RenderMatrix buildTranslate(final double t1, final double t2, final double t3) {
		final RenderMatrix result = new RenderMatrix();
		result.matrix[0][3] = t1;
		result.matrix[1][3] = t2;
		result.matrix[2][3] = t3;
		return result;
	}
	
	/**
	 * @param theta The degree of rotation in radians.
	 * @return a rotation matrix for the x axis. 
	 */
	public static RenderMatrix buildRotateX(final double theta) {
		final RenderMatrix result = new RenderMatrix();
		final double sin = Math.sin(theta);
		final double cos = Math.cos(theta);
		
		// represents the array better this way.
		result.matrix[1][1] = cos; result.matrix[1][2] = -sin;
		result.matrix[2][1] = sin; result.matrix[2][2] = cos;
		
		return result;
	}
	
	/**
	 * @param theta The degree of rotation in radians.
	 * @return a rotation matrix for the y axis. 
	 */
	public static RenderMatrix buildRotateY(final double theta) {
		final RenderMatrix result = new RenderMatrix();
		final double sin = Math.sin(theta);
		final double cos = Math.cos(theta);
		
		// represents the array better this way.
		result.matrix[0][0] = cos; result.matrix[0][2] = sin;
		result.matrix[2][0] = -sin; result.matrix[2][2] = cos;
		
		return result;
	}
	
	/**
	 * @param theta The degree of rotation in radians.
	 * @return a rotation matrix for the z axis. 
	 */
	public static RenderMatrix buildRotateZ(final double theta) {
		final RenderMatrix result = new RenderMatrix();
		final double sin = Math.sin(theta);
		final double cos = Math.cos(theta);
		
		// represents the array better this way.
		result.matrix[0][0] = cos; result.matrix[0][1] = -sin;
		result.matrix[1][0] = sin; result.matrix[1][1] = cos;
		
		return result;
	}
	
	public static RenderMatrix buildFrustum(final double left, final double right, final double top,
											final double bottom, final double near, final double far) {
		final RenderMatrix result = new RenderMatrix();
		final double near_2 = 2 * near;
		// build the frustum projection matrix
		/*c[0][0] = 2.0*zNear/(right - left);
	    c[0][2] = (right + left)/(right - left);
	    c[1][1] = 2.0*zNear/(top - bottom);
	    c[1][2] = (top + bottom)/(top - bottom);
	    c[2][2] = -(zFar + zNear)/(zFar - zNear);
	    c[2][3] = -2.0*zFar*zNear/(zFar - zNear);
	    c[3][2] = -1.0;
	    */
	    result.matrix[0][0] = 2.0*near/(right - left);
	    result.matrix[0][2] = (right + left)/(right - left);
	    result.matrix[1][1] = 2.0*near/(top - bottom);
	    result.matrix[1][2] = (top + bottom)/(top - bottom);
	    result.matrix[2][2] = -(far + near)/(far - near);
	    result.matrix[2][3] = -2.0*far*near/(far - near);
	    result.matrix[3][2] = -1.0;
	    result.matrix[3][3] = 0.0;  // bug fixed by J. Mayer
		
		return result;
	}
	
	public static RenderMatrix buildOrtho(final double left, final double right, final double top,
										  final double bottom, final double near, final double far) {
		final RenderMatrix result = new RenderMatrix();
		
		result.matrix[0][0] = 2.0/(right - left);
		result.matrix[1][1] = 2.0/(top - bottom);
		result.matrix[2][2] = 2.0/(near - far);
		result.matrix[3][3] = 1.0;
		result.matrix[0][3] = -(right + left)/(right - left);
		result.matrix[1][3] = -(top + bottom)/(top - bottom);
		result.matrix[2][3] = -(far + near)/(far - near);
		
		return result;
	}
	
	public static RenderMatrix buildLookAt(final double eyeX, final double eyeY, final double eyeZ,
										   final double atX, final double atY, final double atZ,
										   final double upX, final double upY, final double upZ) {
		RenderMatrix lookAt = new RenderMatrix();
		final double[] n = normalize(eyeX - atX, eyeY - atY, eyeZ - atZ, 0);
		final double[] u = normalize(cross(new double[]{upX, upY, upZ, 1}, n));
		final double[] v = normalize(cross(n, u));
		final double[] t = {0,0,0,1};
		
		// load into lookAt matrix
		for (int i = 0; i < SIZE - 1; i++) {
			lookAt.matrix[0][i] = u[i];
			lookAt.matrix[1][i] = v[i];
			lookAt.matrix[2][i] = n[i];
		}
		
		// final RenderMatrix neg_eye = new RenderMatrix();
//		lookAt.matrix[0][3] = -eyeX;
//		lookAt.matrix[1][3] = -eyeY;
//		lookAt.matrix[2][3] = -eyeZ;
//		lookAt.matrix[3][3] = 1;	
		RenderMatrix translate = new RenderMatrix().buildTranslate(-eyeX, -eyeY, -eyeZ);
		lookAt = lookAt.multiply(translate);
		return lookAt;
	}
	private static double[] normalize(final double[] vector) {
		return normalize(vector[0], vector[1], vector[2], vector[3]);
	}

	private static double[] normalize(final double x, final double y, final double z, final double w) {
		final double mag = Math.sqrt((x*x + y*y + z*z + w*w));
		final double[] norm = {x / mag, y / mag, z / mag, w/mag};
		return norm;
	}

	private static double[] cross(final double[] left_op, final double[] right_op) {
		final double[] cross = {(left_op[1]*right_op[2] - left_op[2]*right_op[1]),
								(left_op[2]*right_op[0] - left_op[0]*right_op[2]), 
								(left_op[0]*right_op[1] - left_op[1]*right_op[0]), 0};
		return cross;
	}

	/**
	 * @param args
	 */
	/*public static void main(String[] args) {
		final RenderMatrix ident = new RenderMatrix();
		final RenderMatrix scale = buildScale(6,6,6);
		final RenderMatrix translate = buildTranslate(10,10,10); 
		
		final RenderMatrix rotatex = buildRotateX(Math.PI/4);
		final RenderMatrix rotatey = buildRotateY(Math.PI/4);
		final RenderMatrix rotatez = buildRotateZ(Math.PI/4); 
		
		// identiy
		//System.out.println("Identity Matrix\n" + ident.toString() + "\n");
		//System.out.println("Scale Matrix\n" + scale.toString() + "\n");
		//System.out.println("Translate Matrix\n" + translate.toString() + "\n");
	
		// multiply test
		//System.out.println("Scale x Translate Matrix\n" + scale.multiply(translate).toString() + "\n");
		//System.out.println("Translate x Scale Matrix\n" + translate.multiply(scale).toString() + "\n");
		
		//System.out.println("RotateX Matrix\n" + rotatex.toString() + "\n");
		//System.out.println("RotateY Matrix\n" + rotatey.toString() + "\n");
		//System.out.println("RotateZ Matrix\n" + rotatez.toString() + "\n");
		
		//System.out.println("RotX x RotY x RotZ\n" + rotatex.multiply(rotatey).multiply(rotatez).toString()+ "\n");
	
		final double[] vector = {0, 0, 1, 1};
		//System.out.println("RotX x (0, 0 ,1)\n" + vectorToString(rotatex.multiply(vector)) + "\n");
		
		vector[2] = 0;
		vector[1] = 1;
		//System.out.println("RotX z (0, 1 ,0)\n" + vectorToString(rotatez.multiply(vector)) + "\n");
		
		vector[2] = 0;
		vector[1] = 0;
		vector[0] = 1;
		//System.out.println("RotX y (1, 0 ,0)\n" + vectorToString(rotatey.multiply(vector)) + "\n");
	}*/

	public static String vectorToString(final double[] vector) {
		if (vector.length != 4) throw new IllegalArgumentException("param is not of length 4");
		return "[" + vector[0] + " ," + vector[1] + " ," + vector[2] + " ," + vector[3] +"]";
	}
	
}
